查詢存放區是一個非常強大的效能調校工具簡單來說,如果把 QUERY STORE 啟用,那原本存在 PLAN CACHE 的東西,就會變成實際存去 TABLE 裡面,用意是讓以後為了效能調教,去找到這些會消失的資訊。
他跟擴充事件的差別就是擴充事件只能抓當下發生,雖然也是可以存起來,但是會消耗更多效能,以及
最早於 2016 推出,在此版本之前 SQL Server 沒有這個功能。
主要功能是 :針對特定資料庫內執行的查詢,蒐集並彙總相關資訊,當然包括查詢效能指標&等候統計資料。
另外一個重點是,他可以保存這些查詢所使用的執行計畫。
還有他可以強制某查詢用某個執行計畫。
從 2022 開始預設啟用,2025開始高可用群組中的可讀取次要副本,也預設啟用。
但是在高可用群組中的次要副本是唯讀的,所以在那邊的查詢存放其實會存在主要資料庫中,然後透過同步機制寫進去次要副本。
--可以用這個打開
ALTER DATABASE AdventureWorks2022
SET QUERY_STORE = ON;
它的運作方式是 :
在最佳化階段,以非同步方式,把執行計畫相關資訊寫入存放區
在執行階段,以非同步方式,把執行計階段統計資料寫入存放區
在一般情況下,查詢存放區並不會直接影響查詢最佳化流程,當查詢提交到系統之後,他會依照以前說過的那個流程去產生執行計畫,然後把那個執行計畫存在 plan cache 中。
不過,查詢存放區的強制執行計畫功能,就會改變這個標準行為。
第一步,當執行計畫存到 plan cache 中之後,SQL Server 會透過一個非同步機制,把這個執行計畫存到另一個獨立的記憶體區域中,然後另一個非同步程序,再把這個執行計畫寫入資料庫 table 中的查詢存放區。
第二步,當查詢按照正常流程開始執行的時候,SQL Server 也會透過非同步程序,把持續時間、讀取量、等候統計資料等等的執行階段指標,也寫入另一個獨立的記憶體區域。
第三步,這些資料會在儲存的時候進行彙總,最後由另一個非同步程序,確保這些資訊寫入 table 。
預設彙總的時間是 60 分鐘,可以依照需求去調高調低。
預設寫入存放區的時間是 15 分鐘,也可以依照需求去調整。
查詢存放區的資訊也會隨著該資料庫一起保存、備份、還原,一起存在 mdf 中。
因為他是非同步,但是不用擔心如果今天去查查詢存放區,SQL Server 會同時回傳 :
仍還在記憶體中的資料
已經寫入硬碟的資料
這個整合過程自動完成,不需要額外進行任何操作。
查詢存放的核心單位是 : 每一個個別的查詢陳述式
就算你是 sp、或是包含多個查詢的批次,但這都無所謂,因為她還是會以個別查詢陳述式為單位去蒐集資訊。
查詢存放區的資訊主要儲存在下列七個系統物件中:
sys.query_store_querysys.query_store_query_textsys.query_store_plansys.query_store_runtime_statssys.query_store_wait_statssys.query_store_runtime_stats_intervalsys.database_query_store_options-- 利用查詢存放區看某個 sp 的資訊
SELECT
qsq.query_id AS [查詢識別碼],
qsq.object_id AS [物件識別碼],
qsqt.query_sql_text AS [查詢文字],
qsp.plan_id AS [執行計畫識別碼],
CAST(qsp.query_plan AS XML) AS [執行計畫]
FROM sys.query_store_query AS qsq
JOIN sys.query_store_query_text AS qsqt
ON qsq.query_text_id = qsqt.query_text_id
JOIN sys.query_store_plan AS qsp
ON qsp.query_id = qsq.query_id
WHERE qsq.object_id =
OBJECT_ID('sp 名稱');
/*
雖然每一個個別的查詢陳述式都儲存在 sys.query_store_query 中,
但該系統檢視同時也會記錄 object_id。
因此,可以像範例中一樣,使用 OBJECT_ID() 函數,透過物件名稱找出指定的預存程序。
另外,範例也對 query_plan 欄位使用了 CAST 轉型
這是因為查詢存放區會將 query_plan 欄位儲存為 NVARCHAR(MAX),
而不是直接儲存成 XML 資料型別。
這樣的設計是合理的,因為 SQL Server 的 XML 資料型別具有巢狀層級限制。
*/
在 DMVs 中,也可以看到類似的設計。SQL Server 提供兩種取得執行計畫的方式:
sys.dm_exec_query_plan
sys.dm_exec_text_query_plan
sys.dm_exec_query_plan 會以 XML 格式回傳執行計畫,但可能受到 XML 巢狀層級限制。
sys.dm_exec_text_query_plan 則以文字格式回傳執行計畫,因此可避免部分 XML 限制。
用這些查詢存放器去擷取查詢會有一個問題是,他擷取到的查詢,有可能會被參數化
例如 :
--我做一個這個查詢
SELECT
a.AddressID,
a.AddressLine1
FROM Person.Address AS a
WHERE a.AddressID = 72;
--然後去看存放器的結果
SELECT
qsq.query_id AS [查詢識別碼],
qsq.object_id AS [物件識別碼],
qsqt.query_sql_text AS [查詢文字],
qsp.plan_id AS [執行計畫識別碼],
TRY_CAST(qsp.query_plan AS XML) AS [執行計畫]
FROM sys.query_store_query AS qsq
JOIN sys.query_store_query_text AS qsqt
ON qsq.query_text_id = qsqt.query_text_id
LEFT JOIN sys.query_store_plan AS qsp
ON qsp.query_id = qsq.query_id
ORDER BY qsq.last_execution_time DESC;
(@1 tinyint)
SELECT [a].[AddressID],[a].[AddressLine1]
FROM [Person].[Address] [a]
WHERE [a].[AddressID]=@1
--他擷取的查詢是這樣參數化的
這是參數化的結果但這會導致你看不出來這是什麼參數而給出的執行計畫
查詢本身跟執行計畫都是非常重要的資訊,但還有一樣東西一直沒提到就是 :
執行階段指標
首先,執行階段指標是對應到特定的執行計畫,不是直接對應到查詢。
因為同一個查詢的不同執行計畫,可能會有不同的執行行為,因此 Query Store 會針對各個執行計畫分別蒐集執行階段指標。
對沒錯不用懷疑,同一查詢是有可能會有不同執行計畫的。
執行階段指標會依照執行階段統計區間進行彙總。預設區間長度為 60 分鐘。這表示如果查詢在某個區間內有執行,該區間就會有一組對應的執行階段指標
由於這些資訊會依照不同的時間區間進行彙總,因此有時還需要再對這些彙總結果進一步加總或取平均值。
雖然這看起來很麻煩,但以時間區間進行彙總其實非常有幫助。因為資料會被拆分成很多個區間,所以可以取得多個比較基準,進而追蹤某個查詢的執行行為如何隨時間改變。
然後透過這些比較基準,可以判斷查詢效能是在惡化還是改善。
第一次讀到這裡會很難理解這些理論定義是什麼,什麼60分鐘 15分鐘的
我是很想寫得很簡略但是都到了效能調教了,所以我還是要寫的準確一點
比較好理解的方式是例如說有一個查詢 A
他產稱了一個執行計畫 X
並且在15分鐘內這個執行計畫 X 被執行了 150 次
那QUERY STORE 就會去記錄這 150 次的平均時間、平均CPU TIME、平均 IO 等等資訊
那如果有查詢 A
同時也產生執行計畫 Y
那同樣的也會去記錄這個 Y 的平均時間等等
所以他是 GROUP BY 執行計畫的,不是 GROUP BY 查詢的
那60分鐘的意思是,QUERY STORE 會把一個 60 分鐘區段當作一個單位
所以按照預設,一個單位裡面就會有4組平均,因為15分鐘嘛
意思是這樣。
--範例 擷取某個特定時間點的執行階段指標
DECLARE @CompareTime DATETIME = '2025-05-29 12:22';
SELECT
CAST(qsp.query_plan AS XML) AS [執行計畫],
qsrs.count_executions AS [執行次數],
qsrs.avg_duration AS [平均執行時間],
qsrs.stdev_duration AS [執行時間標準差],
qsws.wait_category_desc AS [等候類別],
qsws.avg_query_wait_time_ms AS [平均查詢等候時間毫秒],
qsws.stdev_query_wait_time_ms AS [查詢等候時間標準差毫秒]
FROM sys.query_store_plan AS qsp
JOIN sys.query_store_runtime_stats AS qsrs
ON qsrs.plan_id = qsp.plan_id
JOIN sys.query_store_runtime_stats_interval AS qsrsi
ON qsrsi.runtime_stats_interval_id =
qsrs.runtime_stats_interval_id
LEFT JOIN sys.query_store_wait_stats AS qsws
ON qsws.plan_id = qsrs.plan_id
AND qsws.plan_id = qsp.plan_id
AND qsws.execution_type = qsrs.execution_type
AND qsws.runtime_stats_interval_id =
qsrs.runtime_stats_interval_id
WHERE @CompareTime BETWEEN qsrsi.start_time
AND qsrsi.end_time;
--彙整某個查詢的所有效能指標
WITH QSAggregate
AS
(
SELECT
qsrs.plan_id AS [執行計畫識別碼],
SUM(qsrs.count_executions) AS [總執行次數],
AVG(qsrs.avg_duration) AS [平均執行時間],
AVG(qsrs.stdev_duration) AS [執行時間標準差],
qsws.wait_category_desc AS [等候類別],
AVG(qsws.avg_query_wait_time_ms) AS [平均查詢等候時間毫秒],
AVG(qsws.stdev_query_wait_time_ms) AS [查詢等候時間標準差毫秒]
FROM sys.query_store_runtime_stats AS qsrs
LEFT JOIN sys.query_store_wait_stats AS qsws
ON qsws.plan_id = qsrs.plan_id
AND qsws.runtime_stats_interval_id =
qsrs.runtime_stats_interval_id
--這裡使用 LEFT JOIN,是因為不一定每次都會有等候統計資料。
--Query Store 只會擷取超過 1 毫秒的等候。
GROUP BY
qsrs.plan_id,
qsws.wait_category_desc
)
SELECT
CAST(qsp.query_plan AS XML) AS [執行計畫],
qsa.[執行計畫識別碼],
qsa.[總執行次數],
qsa.[平均執行時間],
qsa.[執行時間標準差],
qsa.[等候類別],
qsa.[平均查詢等候時間毫秒],
qsa.[查詢等候時間標準差毫秒]
FROM sys.query_store_plan AS qsp
JOIN QSAggregate AS qsa
ON qsa.[執行計畫識別碼] = qsp.plan_id
WHERE qsp.plan_id = 11;
控制的語法如下,都很簡單,我是覺得也不用背,用久了就會了,或是直接問 AI。
--停用查詢存放區
ALTER DATABASE AdventureWorks2022
SET QUERY_STORE OFF;
--清除查詢存放區資料
ALTER DATABASE AdventureWorks2022
SET QUERY_STORE CLEAR;
--也可以只刪除特定的查詢或執行計畫
EXEC sys.sp_query_store_remove_query
@query_id = @QueryId;
EXEC sys.sp_query_store_remove_plan
@plan_id = @PlanID;
--因為查詢存放是非同步處理,所以也有一個功能可以強制flush到硬碟
EXEC sys.sp_query_store_flush_db;
--檢視目前查詢存放區的設定
SELECT * FROM sys.database_query_store_options;
--修改查詢存放區的最大儲存空間
ALTER DATABASE AdventureWorks2022
SET QUERY_STORE
(
MAX_STORAGE_SIZE_MB = 200,
SIZE_BASED_CLEANUP_MODE = AUTO --當容量滿的時候自動清理,如果設定 OFF,容量滿時會變成唯讀
);
在 SQL Server 2019 之前,Query Store 對於如何擷取查詢,只有三種選項
預設是 all,表示擷取所有查詢
也可以設定成 None,表示 Query Store 仍然保持啟用,因此仍然可以使用強制執行計畫功能,但是會停止擷取新的資料。
最後是 AUTO,這會讓 Query Store 只擷取符合下列任一條件的查詢 :
這可以降低 Quert Store 額外負擔,也能與 Optimize for Ad Hoc Workloads 搭配使用。這個還沒講過這是用來管理計畫快取記憶體使用情況的功能。
還有一個是自訂,這個是2019 以後才有的新功能,用這個選項的話會有四個可以控制的設定 :
這是為了控制最終要保存多少資料,就像最一開始講的,要擷取有必要的東西去調校才有意義。
除了向前面一樣用 T-SQL 查看擷取資訊外,SSMS 有多種內建自訂報表。
報表點進去看一看,這裡說明最常使用的 “資源耗用量排名在前的查詢”

可以將滑鼠游標移到柱狀圖上,可以看到更多相關資訊
由上角有一個設定,可以讓指標選擇不同的彙總方式去看
具體能玩出什麼花樣有很多,不細講了
但這些都是替代 T-SQL 的一種,我最長的使用方式還是用 T-SQL 去看,還可以叫 AI 寫比較方便。
Query Store 大部分功能都是在查詢存放區的功能那裏面提到的
不過他跟擴充事件、DMV之類的很相似
但有一個重要功能是只有 Query Store 才有的就是強制執行計畫
很字面上的意思,就是強制某個查詢使用這個執行計畫,在最佳化階段 sql server 會去檢查有沒有這個強制執行計畫,有的話就用沒有的話就自己產。
這個強制執行計畫的主要目的不是要你自己去生執行計畫然後來說這個計畫比較好以後都用這個。
他是用來讓查詢維持一致的執行行為,不要讓重新編譯之後,產生不同的執行計畫進而對系統造成負面影響。
所以執行計畫原則還是 sql server 最佳化引擎產出,你要把他調到好,然後接下來才去強制這些查詢以後都用這個執行計畫。
--要使用的化很簡單,提供 query_id 根 plan_id 就可
EXEC sys.sp_query_store_force_plan
121,
11
--執行這個命令後,只要該查詢再次編譯或重新編譯,
--SQL Server 就會嘗試使用識別碼為 11 的執行計畫。
--這會讓查詢id 是 121的以後都強制使用執行計畫id 是 11 的這個
--如果查詢計畫去改 where = 這個參數,id 是有可能改變的
--要避免這種狀況通常是參數化 where id = @id 這樣
--但參數化有一個缺點就是前面說的無法使用統計資訊,不過今天是為了固定執行計畫,
--所以代表執行計畫已經調校過了,那這個時候無法使用統計資訊就沒那麼重要了
但有一個很少見的情況是,他可能會用另外一個幾乎完全相同的執行計畫,只有一點點差異,微軟把那個叫做等價值行計畫,很少見但還是會發生。
Query Hint 翻譯叫做查詢提示,很奇怪他不是提示,他是下達命令的感覺。
Query Hint 會限制查詢最佳化器選擇的方案。因此,只有在經過完整測試,並且確定沒有其他更好的方法,才應該使用,而且要謹慎。
我會有一篇專門講解 T-SQL 該怎麼寫的篇章,會詳細的說明為什麼我不建議用 QUERY HINT
從 2022 開始,可以在 Query Store 去做 Query Hint。
--示範 針對 query id = 550去添加OPTIMIZE FOR UNKNOWN
--這個OPTIMIZE FOR UNKNOWN 常用來處理不良參數探嗅,之後再說這是啥
EXEC sys.sp_query_store_set_hints
550,
N'OPTION(OPTIMIZE FOR UNKNOWN)';
那這個效果,跟你直接在 SELECT 最後面去家這個 OPTION 一樣,但透過 QUERY STORE 去做的好處是不用修改原始碼,就可以套用這個提示。
--查目前套用了那些提示,這個套用在執行計畫上面看不到,只能用這個方式去查
SELECT
qsqh.query_hint_id AS [查詢提示識別碼],
qsqh.query_id AS [查詢識別碼],
qsqh.query_hint_text AS [查詢提示內容],
qsqh.source_desc AS [提示來源]
FROM sys.query_store_query_hints AS qsqh;
--移除套用的方法
EXEC sys.sp_query_store_clear_hints
@query_id = 550;
智慧型查詢處理包含很多針對常見查詢效能問題的內部改進,這是一個大功能,之後再說。
但這其中有一個常見問題,就是最佳化流程本身,有時候產生執行計畫可能會消耗大量資源。
因此,從資料庫相容層級 160 開始,也就是 2022 開始,微軟改變了部分執行計畫的產生方式。
當 SQL Server 產生執行計畫,而且最佳化過程超過某些內部門檻時,系統會將部分最佳化資訊,儲存在 Query Store 執行計畫 XML 的隱藏屬性中。
其中保存的是最佳化流程的「重播指令碼」,可在後續需要重新產生該執行計畫時,加快最佳化速度。
這項設計的取捨,是以額外的儲存空間,換取較低的處理成本。
查詢最佳化器會先估算最佳化流程所需的時間。如果實際使用的資源超出預估,而且物件數量、聯結數量、最佳化工作數量及最佳化時間等指標超過內部門檻,系統就會保存該重播指令碼。
例如說有一個很複雜的查詢,第一次跑到最佳化流程的時候,最佳化就會開始想現在是要
A join B 還是 B join C?
Nested Loops?
Hash Join?
Merge Join?
先 Filter 哪張表?
用哪個 Index?
Join 順序怎麼排?
這些過程是很耗時間的,但是 QUERY STORE 這個強制最佳化的功能,就是把這次的選擇記錄下來,下一次又遇到一樣的狀況要編譯的時候,他可以直接省略這些步驟,達到節省編譯時間的目的,進而提升效能。
2022 以後預設是啟用
--啟用
ALTER DATABASE SCOPED CONFIGURATION
SET OPTIMIZED_PLAN_FORCING = ON;
--停用
ALTER DATABASE SCOPED CONFIGURATION
SET OPTIMIZED_PLAN_FORCING = OFF;
--也可以用 QUERY HINT 去停用
DISABLE_OPTIMIZED_PLAN_FORCING
並非所有查詢都能使用強制最佳化。若查詢最佳化流程不是 FULL,該查詢就無法使用這項功能。分散式查詢不符合使用資格,帶有 RECOMPILE 提示的查詢也不支援。
--我們沒辦法看到重播指令的內容,但可以查出那些查詢與執行計畫擁有重播指令碼。
SELECT
qsqt.query_sql_text AS [查詢文字],
TRY_CAST(qsp.query_plan AS XML) AS [執行計畫],
qsp.is_forced_plan AS [是否為強制執行計畫]
FROM sys.query_store_plan AS qsp
INNER JOIN sys.query_store_query AS qsq
ON qsp.query_id = qsq.query_id
INNER JOIN sys.query_store_query_text AS qsqt
ON qsq.query_text_id = qsqt.query_text_id
WHERE qsp.has_compile_replay_script = 1;
最常用查詢存放區的功能是監控、調校查詢效能
第二是用來強制執行計畫
第三就是這個,把這個用來當作 SQL Server 升級期間充當安全網。
假設現在要把 2016 升級到 2025。一般做法都是先測試環境中升級,然後再做一系列測試,確認系統正常運作。
若能找出並記錄所有問題,當然很好。但由於查詢最佳化器或基數估算器可能有所變更,部分查詢可能需要重寫。這可能延後升級時程,甚至讓企業決定完全避免升級;這種選擇雖然常見,但通常並不理想。
例如 2014 大改估算邏輯。
而且,前提還是你真的有發現所有問題。某個查詢可能因估算資料列數改變或其他原因,突然開始出現效能問題,但測試時未必能立即察覺。
這正是 Query Store 能在升級過程中發揮安全網作用的地方。
首先,仍應照常完成所有測試,並以標準方法處理問題。這部分不應改變。不過,Query Store 能在既有流程之外,提供額外的保護機制。
建議依照以下步驟進行:
這種方法無法避免所有問題,因此仍然必須完整測試系統。
不過,Query Store 可以協助你處理 SQL Server 內部變更所造成的執行計畫變化,以及後續產生的效能影響。
類似流程也可以用於套用 SQL Server 累積更新(Cumulative Update)。
更新的核心邏輯就是以下這幾句話
更新之前打開QUERY STORE 蒐集執行計畫
更新之後讓系統正常運作一下,這時候就可以去找找看有沒有更新之後突然變得很慢的查詢
然後先強制他們套用舊版的執行計畫,解決燃眉之急
最後再去針對這些變慢的查詢慢慢調